//
// Created by HP on 2022/1/22.
//

#include "simple_render_system.h"

// libs
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>

// std
#include <stdexcept>

namespace lve {

    // scalar alignment
    // A scalar of size N has a scalar alignment of N
    // A vector or matrix type has a scalar alignment equal to its component type
    // An array type has a scalar alignment equal to that of its element type
    // A structure has a scalar alignment equal to the largest alignment of any of its members
    // base alignment
    // A scalar has a base alignment equal to its scalar alignment
    // A two-component vector has a base alignment equal to twice its scalar alignment
    // A three- or four-component vector has a base alignment equal to four times its scalar alignment
    // An array has a base alignment equal to the base alignment of its element type
    // ...
    // see https://www.khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap15.html#interfaces-resources-layout
    struct SimplePushConstantData {
        alignas(16) glm::mat4 transform{1.0f};
        alignas(16) glm::mat4 normalMatrix{1.0f};  // 对齐要求 16 bytes
    };

    SimpleRenderSystem::SimpleRenderSystem(LveDevice& device, VkRenderPass renderPass): lveDevice{device} {
        createPipelineLayout();
        createPipeline(renderPass);
    }

    SimpleRenderSystem::~SimpleRenderSystem() {
        vkDestroyPipelineLayout(lveDevice.device(), pipelineLayout, nullptr);
    }

    void SimpleRenderSystem::createPipelineLayout() {

        // push constant
        // 有一个专门的、固定大小的内存区域储存  push 的数据
        VkPushConstantRange pushConstantRange{};
        // stateFlags: 设置这个 pushConstant 对于哪一个 Shader 是可见的
        pushConstantRange.stageFlags =
                VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
        // 这一个 range 是从区域的哪里开始
        pushConstantRange.offset = 0;
        pushConstantRange.size = sizeof(SimplePushConstantData);

        VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
        pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
        // PipelineSetLayout 用于传输除了 vertex 以外的数据到 vertShader 和 fragShader 中
        // 如: texture、 uniform buffer object
        pipelineLayoutInfo.setLayoutCount = 0;
        pipelineLayoutInfo.pSetLayouts = nullptr;
        // pushConstant 用于高效地向 shader 传输少量数据
        pipelineLayoutInfo.pushConstantRangeCount = 1;
        pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
        if (vkCreatePipelineLayout(lveDevice.device(),
                                   &pipelineLayoutInfo,
                                   nullptr, &pipelineLayout) != VK_SUCCESS) {
            throw std::runtime_error{"failed to create pipeline layout."};
        }
    }

    void SimpleRenderSystem::createPipeline(VkRenderPass renderPass) {
        assert(pipelineLayout != nullptr && "Cannot create pipeline before pipeline layout.");

        // 同 swapChain 获取宽和高: 不一定和窗口等大
        PipelineConfigInfo pipelineConfig{};
        LvePipeline::defaultPipelineConfig(pipelineConfig);
        // renderPass 描述了 RenderBufferObject 以及其 attachments 的结构和格式
        pipelineConfig.renderPass = renderPass;
        pipelineConfig.pipelineLayout = pipelineLayout;
        lvePipeline = std::make_unique<LvePipeline>(
                lveDevice,
                "F:/vulkan-tutorial/shaders/simple_shader.vert.spv",
                "F:/vulkan-tutorial/shaders/simple_shader.frag.spv",
                pipelineConfig
                );
    }

    void SimpleRenderSystem::renderGameObjects(
            VkCommandBuffer commandBuffer, std::vector<LveGameObject>& gameObjects, const LveCamera& camera) {
        lvePipeline->bind(commandBuffer);

        auto projectionView = camera.getProjection() * camera.getView();

        for (auto& obj: gameObjects) {
            // push constant
            SimplePushConstantData push {};
            auto modelMatrix = obj.transform.mat4();
            push.transform = projectionView * modelMatrix;
            push.normalMatrix = obj.transform.normalMatrix();

            vkCmdPushConstants(
                    commandBuffer,
                    pipelineLayout,
                    VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
                    0,
                    sizeof(SimplePushConstantData),
                    &push
            );
            obj.model->bind(commandBuffer);
            obj.model->draw(commandBuffer);
        }
    }
}
